iT邦幫忙

2023 iThome 鐵人賽

DAY 28
0
Software Development

掌握Java神器,駕馭SpringBoot猛獸系列 第 28

第28日 來做管理後台登入

  • 分享至 

  • xImage
  •  

第28天用昨日寫好的Java檔案,當作終端指令創建管理者帳號後,用新建的帳號來做管理後台的登入驗證

Spring Security 驗證後台管理 API請求

現在回到懷念重複的操作 打開SecurityConf.java,在請求驗證鍊新增管理者專用路由,排除後台登入

    authorize
        // 管理者登入不做驗證
        .requestMatchers("/api/admin/login").permitAll()
        // api/admin/xxx 的請求,除了login都需經過驗證
        .requestMatchers("/api/admin/**").authenticated()
        .requestMatchers("/api/member/login").permitAll()
        .requestMatchers("/api/member/register").permitAll()
        .requestMatchers("/api/member/**").authenticated()
        .anyRequest().permitAll();

在新增 AdminDetialService

@Service("AdminDetailServ")
public class AdminUserDetailsService implements UserDetailsService {
    @Autowired
    private AdminDao adminMapper;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {

        Admin admin = adminMapper.getByAccount(account);

        if (admin == null) {
            throw new UsernameNotFoundException("Admin Not Found");
        }

        System.out.printf(
                "從 管理者帳號 %s 得到 User ID %d\n",
                account, admin.getId());

        return new User(
                admin.getAccount(),
                admin.getPassword(),
                Collections.emptyList());
    }
}

建立 AdminController 開後台登入API

// AdminController.java

    /**
     * 後台登入API
     * 
     * @return ResponseEntity<Object>
     */
    @PostMapping("/login")
    public ResponseEntity<Object> login(
        @RequestBody LoginAdmin loginAdmin
    ) {

        try {

            // 登入同樣用 AuthenticationManager 進行帳號登入驗證
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    loginAdmin.getAccount(), loginAdmin.getPassword());
            authenticationManager.authenticate(authentication);

            // 另外自定義 Jwt Payload
            Map payload = new HashMap<>();
            payload.put("stage", "dashboard");
            String logingToken = jwtUtil.generateToken(loginAdmin.getAccount(), payload);

            return ResponseEntity.status(HttpStatus.OK).body(Map.of(
                    "status", true,
                    "token", logingToken));
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
                    "status", false,
                    "token", ""));
        }
    }

運行 spring-boot run 會拋出例外
https://ithelp.ithome.com.tw/upload/images/20231011/20161920ZA26JAYkdz.png

原因是掃描到到多個實作 UserDetailsService 介面的元件,才造成此異常,為了讓 Interface 可以注入到Bean元件中,需要注入Bean元件的部分,要利用 @Qualifier 指定使用的Bean元件別名

先在UserDetailsService實做類別添加別名


// com.app.demo.services.AdminUserDetailsService.java
@Service("AdminDetailServ")
public class AdminUserDetailsService implements UserDetailsService{
    ...略
}

// com.app.demo.services.DBUserDetailsService.java
@Service("MemberDetailServ")
public class DBUserDetailsService implements UserDetailsService {
    ...略
}

接著點開在 SecurityConf 欄位使用 @Qualifier 指定別名注入 UserDetailsService介面的欄位

// com.app.demo.conf.SecurityConf.java
    @Autowired
    @Qualifier("MemberDetailServ")
    public UserDetailsService dbUserService;@Autowired

    @Autowired
    @Qualifier("AdminDetailServ")
    private UserDetailsService adminService;

    // 添加依賴元件 HttpServletRequest 取得當前請求資訊
    @Autowired
    private HttpServletRequest request;

    // 添加新方法,實作並根據請求 uri前綴取得相應UserDetailsService 實作
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            String uri = request.getRequestURI();
            // 利用請求 Uri 判斷驗證對象來源
            if (uri.startsWith("/api/admin")) {
                return adminService.loadUserByUsername(username);
            }
            return dbUserService.loadUserByUsername(username);
        };
    }

    @Bean
    public AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        // 原本直接套用 dbUserService 實例,這次透過方法彈性改動 UserDetailsService
        provider.setUserDetailsService(userDetailsService());
        return provider;
    }

在重新執行啟用Spring Boot伺服器,個別從 member跟 admin個別的登入端點登入
會根據 userDetailsService()的邏輯,從不同來源取得要驗證的用戶

https://ithelp.ithome.com.tw/upload/images/20231011/20161920qPbcQdEi6l.png

最後在調整過濾器 UserAuthFilter.java 原本只有


    // 原本注入的 DBUserDetailsService 改用 Qualifier注入
    @Autowired
    @Qualifier("MemberDetailServ")
    private UserDetailsService userDetialServ;

    // 使用 AdminDetailServ 注入 AdminUserDetailsService
    @Autowired
    @Qualifier("AdminDetailServ")
    private UserDetailsService adminDetialServ;
    
    // doFilterInternal 部分內容調整,根據 stage 判斷查找前台還是後台使用者
    UserDetails userDetials = null;
    String stage = payload.get("stage", String.class);
    System.out.printf("Claims stage is: %s\n", stage);
    // 管理者帳號用 adminDetialServ 取得User
    if (stage.equals("dashboard")) {
        userDetials = adminDetialServ.loadUserByUsername(account);
    } else {
        userDetials = userDetialServ.loadUserByUsername(account);
    }

    System.out.printf("UserDetails Username %s\n", userDetials.getUsername());

利用登入管理者後台拿到的Token,在隨意請求 /api/admin為前綴的端點,原本403拒絕請求,現在則變成404
https://ithelp.ithome.com.tw/upload/images/20231011/20161920QgP1Q04uVZ.png

今日進度完成,明天來重構 過濾器的部分並在請求添加 Role驗證

參考

  1. Github範例

上一篇
第27日 搭配Spring IoC做Comamnd指令
下一篇
第29日 修正過濾器鍊
系列文
掌握Java神器,駕馭SpringBoot猛獸30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
yale918
iT邦新手 4 級 ‧ 2023-10-11 22:06:40

謝謝蛙哥 我學會了!

我要留言

立即登入留言